using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Reflection;

using Pegasus.Diagnostics;

namespace Pegasus.Runtime.Serialization.Formatters
{
	/// <summary>
	/// This class is used to track object for system formatters.  The .NET api has a ObjectManager that does the
	/// similar function.  I wrote my own version, first off, because I wanted to provide some specific functionality
	/// the MS didn't have and second I wanted to understand how the whole serialization/deserializtion process worked
	/// and the best way to do that is to write some code.
	/// </summary>
	internal class FormatterObjectManager
	{
		// Local Instance Values
		private long m_objectIndex = 1;
		private StreamingContext m_context = new StreamingContext();
		private long m_rootObjectId = 0;
		private Dictionary<int, TypeEventInfo> m_typeTable = new Dictionary<int, TypeEventInfo>();
		private Dictionary<long, object> m_objectTable = new Dictionary<long, object>();
		private Stack<ObjectFixupInfo> m_objectFixupStack = new Stack<ObjectFixupInfo>();


		/// <summary>
		/// Initializes a new instance of the <see cref="T:FormatterObjectManager"/> class.
		/// </summary>
		public FormatterObjectManager()
		{
		}

		/// <summary>
		/// Gets or sets the root object id.
		/// </summary>
		/// <value>The root object id.</value>
		public long RootObjectId
		{
			get
			{
				return m_rootObjectId;
			}
		
			set
			{
				if( m_rootObjectId == 0 )
				{
					m_rootObjectId = value;
				}
			}
		}

		/// <summary>
		/// Gets the object id.
		/// </summary>
		/// <param name="value">The value.</param>
		/// <returns>Returns the Id of the object or 0 (zero) if the object is not registered.</returns>
		public long GetObjectId( object value )
		{
			// Check parameters
			ParamCode.AssertNotNull( value, "value" );

			foreach( KeyValuePair<long, object> entry in m_objectTable )
			{
				if( entry.Value.Equals( value ) )
				{
					return entry.Key;
				}
			}

			return 0;
		}

		/// <summary>
		/// Gets the object id for the given object.  If this is the first time the system has seen the
		/// object it will register it and return the new object id, and set firstTime to true.
		/// </summary>
		/// <param name="value">The value.</param>
		/// <param name="firstTime">If true this is the first the system as see of the object.</param>
		/// <returns>The object id for the object.</returns>
		public long GetObjectId( object value, out bool firstTime )
		{
			// Check parameters
			ParamCode.AssertNotNull( value, "value" );

			long objectId = 0;
			firstTime = false;

			if( m_objectTable.ContainsValue( value ) )
			{
				objectId = GetObjectId( value );
			}
			else
			{
				firstTime = true;

				objectId = m_objectIndex;
				m_objectIndex++;
				
				m_objectTable.Add( objectId, value );
				RootObjectId = objectId;
			}

			return objectId;
		}

		/// <summary>
		/// Gets the object.
		/// </summary>
		/// <param name="objectId">The object id.</param>
		/// <returns></returns>
		public object GetObject( long objectId )
		{
			// Check parameters
			ParamCode.AssertRange( objectId, 1, long.MaxValue, "objectId" );

			return m_objectTable[ objectId ];
		}

		/// <summary>
		/// Registers the object.
		/// </summary>
		/// <param name="value">The value.</param>
		/// <param name="objectId">The object id.</param>
		public void RegisterObject( object value, long objectId )
		{
			// Check parameters
			ParamCode.AssertNotNull( value, "value" );
			ParamCode.AssertRange( objectId, 1, long.MaxValue, "objectId" );

			if( m_objectTable.ContainsKey( objectId ) )
			{
				throw new ArgumentException( "There is an object already registered with the given id" );
			}

			// Move the object index byond the new object id value.
			if( m_objectIndex <= objectId )
			{
				m_objectIndex = objectId + 1;
			}

			m_objectTable.Add( objectId, value );
			RootObjectId = objectId;
		}

		/// <summary>
		/// Replaces the object.
		/// </summary>
		/// <param name="value">The value.</param>
		/// <param name="objectId">The object id.</param>
		public void ReplaceObject( object value, long objectId )
		{
			// Check parameters
			ParamCode.AssertNotNull( value, "value" );
			ParamCode.AssertRange( objectId, 1, long.MaxValue, "objectId" );

			if( !m_objectTable.ContainsKey( objectId ) )
			{
				throw new ArgumentException( "There is no object registered with the given id" );
			}

			m_objectTable[ objectId ] = value;
		}

		/// <summary>
		/// Registers the object reference fixup.
		/// </summary>
		/// <param name="objectId">The object id.</param>
		/// <param name="parentObjectId">The parent object id.</param>
		/// <param name="parentMemberInfo">The parent member info.</param>
		public void RegisterObjectReferenceFixup( long objectId, long parentObjectId, MemberInfo parentMemberInfo )
		{
			if( parentObjectId > 0 && parentMemberInfo != null )
			{
				ObjectFixupInfoMember fixupInfo = new ObjectFixupInfoMember();
				fixupInfo.ObjectId = objectId;
				fixupInfo.ParentObjectId = parentObjectId;
				fixupInfo.ParentMemberInfo = parentMemberInfo;

				m_objectFixupStack.Push( fixupInfo );
			}
		}

		/// <summary>
		/// Registers the object reference fixup.
		/// </summary>
		/// <param name="objectId">The object id.</param>
		/// <param name="array">The array.</param>
		/// <param name="index">The index.</param>
		public void RegisterObjectReferenceFixup( long objectId, Array array, int index )
		{
			if( array != null )
			{
				ObjectFixupInfoArray fixupInfo = new ObjectFixupInfoArray();
				fixupInfo.ObjectId = objectId;
				fixupInfo.Array = array;
				fixupInfo.Index = index;

				m_objectFixupStack.Push( fixupInfo );
			}
		}

		/// <summary>
		/// Fixups the object graph.
		/// </summary>
		public void FixupObjectGraph()
		{
			while( m_objectFixupStack.Count > 0 )
			{
				ObjectFixupInfo fixupInfo = m_objectFixupStack.Pop();
				if( fixupInfo is ObjectFixupInfoMember )
				{
					FixupObjectGraphMember( (ObjectFixupInfoMember) fixupInfo );
				}
				else
				{
					FixupObjectGraphArray( (ObjectFixupInfoArray) fixupInfo );
				}
			}
		}

		/// <summary>
		/// Fires the on serializing event.
		/// </summary>
		/// <param name="value">The value.</param>
		public void FireOnSerializingEvent( object value )
		{
			// Check parameters
			ParamCode.AssertNotNull( value, "value" );

			TypeEventInfo eventInfo = RegisterType( value.GetType() );
			if( eventInfo.OnSerializingMethod != null )
			{
				eventInfo.OnSerializingMethod.Invoke( value, new object[] { m_context } );
			}
		}

		/// <summary>
		/// Fires the on serialized event.
		/// </summary>
		/// <param name="value">The value.</param>
		public void FireOnSerializedEvent( object value )
		{
			// Check parameters
			ParamCode.AssertNotNull( value, "value" );

			TypeEventInfo eventInfo = RegisterType( value.GetType() );
			if( eventInfo.OnSerializedMethod != null )
			{
				eventInfo.OnSerializedMethod.Invoke( value, new object[] { m_context } );
			}
		}

		/// <summary>
		/// Fires the on deserializing event.
		/// </summary>
		/// <param name="value">The value.</param>
		public void FireOnDeserializingEvent( object value )
		{
			// Check parameters
			ParamCode.AssertNotNull( value, "value" );

			TypeEventInfo eventInfo = RegisterType( value.GetType() );
			if( eventInfo.OnDeserializingMethod != null )
			{
				eventInfo.OnDeserializingMethod.Invoke( value, new object[] { m_context } );
			}
		}

		/// <summary>
		/// Fires the on deserialized event.
		/// </summary>
		/// <param name="value">The value.</param>
		public void FireOnDeserializedEvent( object value )
		{
			// Check parameters
			ParamCode.AssertNotNull( value, "value" );

			TypeEventInfo eventInfo = RegisterType( value.GetType() );
			if( eventInfo.OnDeserializedMethod != null )
			{
				eventInfo.OnDeserializedMethod.Invoke( value, new object[] { m_context } );
			}
		}

		/// <summary>
		/// Fires the on deserialization complete event.
		/// </summary>
		public void FireOnDeserializationCompleteEvent()
		{
			foreach( object value in m_objectTable.Values )
			{
				if( value is IDeserializationCallback )
				{
					( (IDeserializationCallback) value ).OnDeserialization( this );
				}
			}
		}

		/// <summary>
		/// Resets this instance.
		/// </summary>
		public void Reset( StreamingContext context )
		{
			m_context = context;

			m_objectIndex = 1;
			m_objectTable.Clear();

			m_rootObjectId = 0;

			m_objectFixupStack.Clear();
		}

		/// <summary>
		/// Registers the type.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns></returns>
		private TypeEventInfo RegisterType( Type type )
		{
			TypeEventInfo ret = null;
			int hashCode = type.GetHashCode();

			// First see if the type is registered
			if( m_typeTable.ContainsKey( hashCode ) )
			{
				ret = m_typeTable[ hashCode ];
			}
			else
			{
				ret = new TypeEventInfo();
				ret.Type = type;
				ret.OnSerializingMethod = GetMethodWithAttribute( type, typeof( OnSerializingAttribute ) );
				ret.OnSerializedMethod = GetMethodWithAttribute( type, typeof( OnSerializedAttribute ) );
				ret.OnDeserializingMethod = GetMethodWithAttribute( type, typeof( OnDeserializingAttribute ) );
				ret.OnDeserializedMethod = GetMethodWithAttribute( type, typeof( OnDeserializedAttribute ) );

				m_typeTable.Add( hashCode, ret );
			}

			return ret;
		}

		/// <summary>
		/// Gets the method with attribute.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <param name="attributeType">Type of the attribute.</param>
		/// <returns></returns>
		private MethodInfo GetMethodWithAttribute( Type type, Type attributeType )
		{
			MethodInfo[] methods = type.GetMethods( BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static );
			foreach( MethodInfo method in methods )
			{
				object[] attribs = method.GetCustomAttributes( attributeType, false );
				if( attribs.Length > 0 )
				{
					return method;
				}
			}

			return null;
		}

		/// <summary>
		/// Fixups the object graph member.
		/// </summary>
		/// <param name="fixupInfo">The fixup info.</param>
		private void FixupObjectGraphMember( ObjectFixupInfoMember fixupInfo )
		{
			IObjectReference objectRef = (IObjectReference) m_objectTable[ fixupInfo.ObjectId ];
			object realObject = objectRef.GetRealObject( m_context );

			object parentObject = m_objectTable[ fixupInfo.ParentObjectId ];

			if( fixupInfo.ParentMemberInfo is FieldInfo )
			{
				FieldInfo fieldInfo = (FieldInfo) fixupInfo.ParentMemberInfo;
				fieldInfo.SetValue( parentObject, realObject );
			}
			else if( fixupInfo.ParentMemberInfo is PropertyInfo )
			{
				PropertyInfo propertyInfo = (PropertyInfo) fixupInfo.ParentMemberInfo;
				propertyInfo.SetValue( parentObject, realObject, null );
			}
			else
			{
				throw new SerializationException( "Unknow member information to fixup object with" );
			}
		}

		/// <summary>
		/// Fixups the object graph array.
		/// </summary>
		/// <param name="fixupInfo">The fixup info.</param>
		private void FixupObjectGraphArray( ObjectFixupInfoArray fixupInfo )
		{
			IObjectReference objectRef = (IObjectReference) m_objectTable[ fixupInfo.ObjectId ];
			object realObject = objectRef.GetRealObject( m_context );

			fixupInfo.Array.SetValue( realObject, fixupInfo.Index );
		}

		/// <summary>
		/// 
		/// </summary>
		private class TypeEventInfo
		{
			public Type Type;
			public MethodInfo OnSerializingMethod;
			public MethodInfo OnSerializedMethod;
			public MethodInfo OnDeserializingMethod;
			public MethodInfo OnDeserializedMethod;
		}

		/// <summary>
		/// 
		/// </summary>
		private class ObjectFixupInfo
		{
			public long ObjectId;
		}

		/// <summary>
		/// 
		/// </summary>
		private class ObjectFixupInfoMember : ObjectFixupInfo
		{
			public long ParentObjectId;
			public MemberInfo ParentMemberInfo;
		}

		/// <summary>
		/// 
		/// </summary>
		private class ObjectFixupInfoArray : ObjectFixupInfo
		{
			public Array Array;
			public int Index;
		}
	}
}
